﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Runtime.InteropServices;

//Copyright (C) 2010 Florian Kolb

//This program is free software; you can redistribute it and/or modify it under the terms 
//of the GNU General Public License as published by the Free Software Foundation; either 
//version 2 of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
//See the GNU General Public License for more details.

//You should have received a copy of the GNU General Public License along with this program; 
//if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA 

namespace Spider_Compiler
{
    public class Compiler
    {
        #region Private Fields
        private const string commandsElement = "commands";
        private const string commandElement = "command";
        private const string innerSet = "!INNER_SET!";
        private const string tmpSourceFile = "spider.cs";
        private const string assemblyInfoFile = "AssemblyInfo.cs";
        private const string assemblyPrefix = "assembly-";
        private const string compileArgsPrefix = "compile-args-"; 
        private const int MAX_PATH = 256;
        private List<CompilerInstruction> instructions;
        private string filename;
        #endregion

        #region Public Fields
        public delegate void CompileProgress(string message);
        public delegate void CompileFinished();
        public event CompileProgress Progress;
        public event CompileFinished Finished;
        #endregion

        public Compiler(List<CompilerInstruction> instructions, string filename)
        {
            this.instructions = instructions;
            this.filename = filename;
        }

        [DllImport("mscoree.dll",
        CharSet = CharSet.Unicode,
        ExactSpelling = true)]
        public static extern int GetCORSystemDirectory(
        StringBuilder buf,
        int cchBuf,
        ref int cchRequired);

        /// <summary>
        /// Function that return .net framework installation path.
        /// This code is from: http://forums.asp.net/t/1448679.aspx
        /// </summary>
        public string GetNetFrameworkDirectory()
        {
            StringBuilder buf = new StringBuilder(
            MAX_PATH, MAX_PATH);
            int cch = MAX_PATH;
            int hr = GetCORSystemDirectory(
            buf, MAX_PATH, ref cch);
            if (hr < 0) Marshal.ThrowExceptionForHR(hr);
            return buf.ToString();
        }

        public void Compile()
        {
            List<string> commands = new List<string>();

            File.Create(tmpSourceFile).Close();
            File.Create(assemblyInfoFile).Close();
            StreamWriter writer = File.AppendText(tmpSourceFile);
            StreamWriter assemblyWriter = File.AppendText(assemblyInfoFile);

            List<CompilerInstruction> assemblyInstructions = new List<CompilerInstruction>();
            assemblyInstructions.AddRange(from item in this.instructions
                                          where item.Id.StartsWith(assemblyPrefix)
                                          select item);

            List<CompilerInstruction> compilerArgs = new List<CompilerInstruction>();
            compilerArgs.AddRange(from item in this.instructions
                                  where item.Id.StartsWith(compileArgsPrefix)
                                  select item);

            Progress("Generating compiler instructions...");

            //Generate the instructions for the C#-Compiler
            GenerateCompilerInstructions(commands, writer);

            //Generate the assembly information
            GenerateAssemblyInformation(assemblyWriter, assemblyInstructions);

            string compileArgs = string.Empty;

            if (compilerArgs != null && compilerArgs.Count > 0)
            {
                foreach (CompilerInstruction instruction in compilerArgs)
                {
                    compileArgs += " " + instruction.Command.Replace("{" + instruction.Parameter.SingleOrDefault().Key + "}", instruction.Parameter.SingleOrDefault().Value.ToString());
                }
            }

            Progress("Compiler instructions generated!");

            //Compile!
            CallCompiler(compileArgs);
        }

        private void CallCompiler(string compileArgs)
        {
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }

            using (Process p = new Process())
            {
                p.StartInfo.FileName = Path.Combine(GetNetFrameworkDirectory(), "csc.exe");
                p.StartInfo.Arguments = string.Format("/out:{1} /target:exe{3} {0} {2} /o+", tmpSourceFile, filename, assemblyInfoFile, compileArgs);
                p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                Progress(string.Format("Compiling to '{0}'...", filename));
                p.Start();

                while (true)
                {
                    //Delete temporary files after compiling
                    if (p.HasExited)
                    {
                        Progress("Deleting temporary files...");
                        File.Delete(tmpSourceFile);
                        File.Delete(assemblyInfoFile);
                        break;
                    }
                }
            }

            if (Finished != null)
                Finished();
        }

        private string ReplaceInvalidCharacters(string expression)
        {
            return expression.Replace("\"", "\\\"").Replace("\\", @"\\");
        }

        private void GenerateCompilerInstructions(List<string> commands, StreamWriter writer)
        {
            foreach (CompilerInstruction instruction in this.instructions)
            {
                if (instruction.Parameter.Count > 0)
                {
                    string tmpCommand = instruction.Command;
                    int i = 0;

                    foreach (KeyValuePair<string, object> exp in instruction.Parameter)
                    {
                        if (!instruction.Id.StartsWith(assemblyPrefix) && !instruction.Id.StartsWith(compileArgsPrefix))
                        {
                            tmpCommand = tmpCommand.Replace("{" + exp.Key + "}", ReplaceInvalidCharacters(exp.Value.ToString()));

                            i++;

                            if (i == instruction.Parameter.Count)
                            {
                                commands.Add(tmpCommand);
                                writer.WriteLine(tmpCommand);
                            }

                        }
                    }
                }
                else
                {
                    if (!instruction.Id.StartsWith(assemblyPrefix) && !instruction.Id.StartsWith(compileArgsPrefix))
                    {
                        string command = instruction.Command;
                        commands.Add(command);
                        writer.WriteLine(command);
                    }
                }
            }

            writer.WriteLine("}\n}");
            writer.Close();
        }

        private void GenerateAssemblyInformation(StreamWriter assemblyWriter, List<CompilerInstruction> assemblyInstructions)
        {
            string assemblyInfo = Properties.Resources.AssemblyInfo;

            //Generating assembly information
            if (assemblyInstructions != null && assemblyInstructions.Count > 0)
            {
                Progress("Creating assembly information...");
                Progress(new string('=', 40));

                foreach (CompilerInstruction instruction in assemblyInstructions)
                {
                    string original = instruction.Id;
                    string replaced = instruction.Id.Replace(assemblyPrefix, string.Empty);
                    string first = replaced[0].ToString().ToUpper();
                    instruction.Id = replaced.Remove(0, 1);
                    instruction.Id = instruction.Id.Insert(0, first);
                    assemblyInfo = assemblyInfo.Replace(instruction.Id + "(\"", instruction.Id + "(\"" + instruction.Parameter[original].ToString());

                    Progress(instruction.Id + ": " + instruction.Parameter[original].ToString());
                }
            }

            if (assemblyInfo.Contains("Version(\"\")"))
            {
                assemblyInfo = assemblyInfo.Replace("Version(\"\")", "Version(\"1.0.0.0\")");
            }

            assemblyInfo = assemblyInfo.Replace("{GUID}", Guid.NewGuid().ToString());
            assemblyWriter.Write(assemblyInfo);
            assemblyWriter.Close();

            Progress(new string('=', 40));
            Progress("Assembly information generated!");
        }
    }
}
